package com.clothing.dao.impl;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Fetch;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;

import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;

import com.clothing.dao.BaseDao;
import com.clothing.entity.OrderEntity;
import com.clothing.utils.page.Filter;
import com.clothing.utils.page.Filter.Operator;
import com.clothing.utils.page.Order;
import com.clothing.utils.page.Order.Direction;
import com.clothing.utils.page.Page;
import com.clothing.utils.page.Pageable;

/**
 * Dao - 基类
 * 
 * @author PTR Team
 * @version 1.0
 */
public abstract class BaseDaoImpl<T, ID extends Serializable> implements
        BaseDao<T, ID>
{

    /** 实体类类型 */
    private Class<T> entityClass;

    /** 别名数 */
    private static volatile long aliasCount = 0;

    /** 查询条件字段类型 */
    private static enum PathType
    {
        /** 通用 */
        common,

        /** string型 */
        string,

        /** 数字型 */
        number
    }

    @PersistenceContext
    protected EntityManager entityManager;

    @SuppressWarnings("unchecked")
    public BaseDaoImpl()
    {
        Type type = getClass().getGenericSuperclass();
        Type[] parameterizedType = ((ParameterizedType) type)
                .getActualTypeArguments();
        entityClass = (Class<T>) parameterizedType[0];
    }

    public T find(ID id)
    {
        if (id != null)
        {
            return entityManager.find(entityClass, id);
        }
        return null;
    }

    public T find(ID id, LockModeType lockModeType)
    {
        if (id != null)
        {
            if (lockModeType != null)
            {
                return entityManager.find(entityClass, id, lockModeType);
            }
            else
            {
                return entityManager.find(entityClass, id);
            }
        }
        return null;
    }

    public List<T> findList(Integer first, Integer count, List<Filter> filters,
            List<Order> orders)
    {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder
                .createQuery(entityClass);
        criteriaQuery.select(criteriaQuery.from(entityClass));
        return findList(criteriaQuery, first, count, filters, orders);
    }

    public Page<T> findPage(Pageable pageable)
    {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder
                .createQuery(entityClass);
        criteriaQuery.select(criteriaQuery.from(entityClass));
        return findPage(criteriaQuery, pageable);
    }

    public long count(Filter... filters)
    {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder
                .createQuery(entityClass);
        criteriaQuery.select(criteriaQuery.from(entityClass));
        return count(criteriaQuery, filters != null ? Arrays.asList(filters)
                : null);
    }

    public void persist(T entity)
    {
        Assert.notNull(entity);
        entityManager.persist(entity);
    }

    public void persist(Collection<T> entitys)
    {
        Assert.notNull(entitys);
        for (T t : entitys)
        {
            persist(t);
        }
    }

    public T merge(T entity)
    {
        Assert.notNull(entity);
        return entityManager.merge(entity);
    }

    public void remove(T entity)
    {
        if (entity != null)
        {
            entityManager.remove(entity);
        }
    }

    public void refresh(T entity)
    {
        if (entity != null)
        {
            entityManager.refresh(entity);
        }
    }

    public void refresh(T entity, LockModeType lockModeType)
    {
        if (entity != null)
        {
            if (lockModeType != null)
            {
                entityManager.refresh(entity, lockModeType);
            }
            else
            {
                entityManager.refresh(entity);
            }
        }
    }

    @SuppressWarnings("unchecked")
    public ID getIdentifier(T entity)
    {
        Assert.notNull(entity);
        return (ID) entityManager.getEntityManagerFactory()
                .getPersistenceUnitUtil().getIdentifier(entity);
    }

    public boolean isManaged(T entity)
    {
        return entityManager.contains(entity);
    }

    public void detach(T entity)
    {
        entityManager.detach(entity);
    }

    public void lock(T entity, LockModeType lockModeType)
    {
        if (entity != null && lockModeType != null)
        {
            entityManager.lock(entity, lockModeType);
        }
    }

    public void clear()
    {
        entityManager.clear();
    }

    public void flush()
    {
        entityManager.flush();
    }

    protected List<T> findList(CriteriaQuery<T> criteriaQuery, Integer first,
            Integer count, List<Filter> filters, List<Order> orders)
    {
        Assert.notNull(criteriaQuery);
        Assert.notNull(criteriaQuery.getSelection());
        Assert.notEmpty(criteriaQuery.getRoots());

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        Root<T> root = getRoot(criteriaQuery);
        addRestrictions(criteriaQuery, filters);
        addOrders(criteriaQuery, orders);
        if (criteriaQuery.getOrderList().isEmpty())
        {
            if (OrderEntity.class.isAssignableFrom(entityClass))
            {
                criteriaQuery.orderBy(criteriaBuilder.asc(root
                        .get(OrderEntity.ORDER_PROPERTY_NAME)));
            }
            else
            {
                criteriaQuery.orderBy(criteriaBuilder.desc(root
                        .get(OrderEntity.ID_PROPERTY_NAME)));
            }
        }
        TypedQuery<T> query = entityManager.createQuery(criteriaQuery)
                .setFlushMode(FlushModeType.COMMIT);
        if (first != null)
        {
            query.setFirstResult(first);
        }
        if (count != null)
        {
            query.setMaxResults(count);
        }
        return query.getResultList();
    }

    protected Page<T> findPage(CriteriaQuery<T> criteriaQuery, Pageable pageable)
    {
        Assert.notNull(criteriaQuery);
        Assert.notNull(criteriaQuery.getSelection());
        Assert.notEmpty(criteriaQuery.getRoots());

        if (pageable == null)
        {
            pageable = new Pageable();
        }
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        Root<T> root = getRoot(criteriaQuery);
        addRestrictions(criteriaQuery, pageable);
        addOrders(criteriaQuery, pageable);
        if (criteriaQuery.getOrderList().isEmpty())
        {
            if (OrderEntity.class.isAssignableFrom(entityClass))
            {
                criteriaQuery.orderBy(criteriaBuilder.asc(root
                        .get(OrderEntity.ORDER_PROPERTY_NAME)));
            }
            else
            {
                criteriaQuery.orderBy(criteriaBuilder.desc(root
                        .get(OrderEntity.ID_PROPERTY_NAME)));
            }
        }

        long total = count(criteriaQuery, null);
        List<T> list = null;

        if (total > 0)
        {
//            int totalPages = (int) Math.ceil((double) total
//                    / (double) pageable.getPageSize());
//            if (totalPages < pageable.getPageNumber())
//            {
//                pageable.setPageNumber(totalPages);
//            }

            TypedQuery<T> query = entityManager.createQuery(criteriaQuery)
                    .setFlushMode(FlushModeType.COMMIT);
            query.setFirstResult((pageable.getPageNumber() - 1)
                    * pageable.getPageSize());
            query.setMaxResults(pageable.getPageSize());
            list = query.getResultList();
        }

        return new Page<T>(list, total, pageable);
    }

    protected Long count(CriteriaQuery<T> criteriaQuery, List<Filter> filters)
    {
        Assert.notNull(criteriaQuery);
        Assert.notNull(criteriaQuery.getSelection());
        Assert.notEmpty(criteriaQuery.getRoots());

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        addRestrictions(criteriaQuery, filters);

        CriteriaQuery<Long> countCriteriaQuery = criteriaBuilder
                .createQuery(Long.class);
        for (Root<?> root : criteriaQuery.getRoots())
        {
            Root<?> dest = countCriteriaQuery.from(root.getJavaType());
            dest.alias(getAlias(root));
            copyJoins(root, dest);
        }

        Root<?> countRoot = getRoot(countCriteriaQuery,
                criteriaQuery.getResultType());
        countCriteriaQuery.select(criteriaBuilder.count(countRoot));

        if (criteriaQuery.getGroupList() != null)
        {
            countCriteriaQuery.groupBy(criteriaQuery.getGroupList());
        }
        if (criteriaQuery.getGroupRestriction() != null)
        {
            countCriteriaQuery.having(criteriaQuery.getGroupRestriction());
        }
        if (criteriaQuery.getRestriction() != null)
        {
            countCriteriaQuery.where(criteriaQuery.getRestriction());
        }
        return entityManager.createQuery(countCriteriaQuery)
                .setFlushMode(FlushModeType.COMMIT).getSingleResult();
    }

    protected synchronized String getAlias(Selection<?> selection)
    {
        if (selection != null)
        {
            String alias = selection.getAlias();
            if (alias == null)
            {
                if (aliasCount >= 1000)
                {
                    aliasCount = 0;
                }
                alias = "appGeneratedAlias" + aliasCount++;
                selection.alias(alias);
            }
            return alias;
        }
        return null;
    }

    private Root<T> getRoot(CriteriaQuery<T> criteriaQuery)
    {
        if (criteriaQuery != null)
        {
            return getRoot(criteriaQuery, criteriaQuery.getResultType());
        }
        return null;
    }

    private Root<T> getRoot(CriteriaQuery<?> criteriaQuery, Class<T> clazz)
    {
        if (criteriaQuery != null && criteriaQuery.getRoots() != null
                && clazz != null)
        {
            for (Root<?> root : criteriaQuery.getRoots())
            {
                if (clazz.equals(root.getJavaType()))
                {
                    return (Root<T>) root.as(clazz);
                }
            }
        }
        return null;
    }

    protected void copyJoins(From<?, ?> from, From<?, ?> to)
    {
        for (Join<?, ?> join : from.getJoins())
        {
            Join<?, ?> toJoin = to.join(join.getAttribute().getName(),
                    join.getJoinType());
            toJoin.alias(getAlias(join));
            copyJoins(join, toJoin);
        }
        for (Fetch<?, ?> fetch : from.getFetches())
        {
            Fetch<?, ?> toFetch = to.fetch(fetch.getAttribute().getName());
            copyFetches(fetch, toFetch);
        }
    }

    private void copyFetches(Fetch<?, ?> from, Fetch<?, ?> to)
    {
        for (Fetch<?, ?> fetch : from.getFetches())
        {
            Fetch<?, ?> toFetch = to.fetch(fetch.getAttribute().getName());
            copyFetches(fetch, toFetch);
        }
    }

    @SuppressWarnings("rawtypes")
    private Path getPath(Path path, PathType type, String property)
    {
        String[] strs = property.split("\\.");

        for (int i = 0; i < strs.length; i++)
        {
            if (i == strs.length - 1)
            {
                if (PathType.string.equals(type))
                {
                    path = path.<String> get(strs[i]);
                }
                else if (PathType.number.equals(type))
                {
                    path = path.<Number> get(strs[i]);
                }
                else
                {
                    path = path.get(strs[i]);
                }
            }
            else
            {
                path = path.get(strs[i]);
            }
        }

        return path;
    }

    @SuppressWarnings("unchecked")
    private void addRestrictions(CriteriaQuery<T> criteriaQuery,
            List<Filter> filters)
    {
        if (criteriaQuery == null || filters == null || filters.isEmpty())
        {
            return;
        }
        Root<T> root = getRoot(criteriaQuery);
        if (root == null)
        {
            return;
        }

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        Predicate restrictions = criteriaQuery.getRestriction() != null ? criteriaQuery
                .getRestriction() : criteriaBuilder.conjunction();
        for (Filter filter : filters)
        {
            if (filter == null || StringUtils.isEmpty(filter.getProperty()))
            {
                continue;
            }
            if (filter.getOperator() == Operator.eq
                    && filter.getValue() != null)
            {
                if (filter.getIgnoreCase() != null && filter.getIgnoreCase()
                        && filter.getValue() instanceof String)
                {
                    restrictions = criteriaBuilder
                            .and(restrictions, criteriaBuilder.equal(
                                    criteriaBuilder.lower(getPath(root,
                                            PathType.string,
                                            filter.getProperty())),
                                    ((String) filter.getValue()).toLowerCase()));
                }
                else
                {
                    restrictions = criteriaBuilder.and(
                            restrictions,
                            criteriaBuilder.equal(
                                    getPath(root, PathType.common,
                                            filter.getProperty()),
                                    filter.getValue()));
                }
            }
            else if (filter.getOperator() == Operator.ne
                    && filter.getValue() != null)
            {
                if (filter.getIgnoreCase() != null && filter.getIgnoreCase()
                        && filter.getValue() instanceof String)
                {
                    restrictions = criteriaBuilder
                            .and(restrictions, criteriaBuilder.notEqual(
                                    criteriaBuilder.lower(getPath(root,
                                            PathType.string,
                                            filter.getProperty())),
                                    ((String) filter.getValue()).toLowerCase()));
                }
                else
                {
                    restrictions = criteriaBuilder.and(
                            restrictions,
                            criteriaBuilder.notEqual(
                                    getPath(root, PathType.common,
                                            filter.getProperty()),
                                    filter.getValue()));
                }
            }
            else if (filter.getOperator() == Operator.gt
                    && filter.getValue() != null)
            {
                restrictions = criteriaBuilder.and(
                        restrictions,
                        criteriaBuilder.gt(
                                getPath(root, PathType.number,
                                        filter.getProperty()),
                                (Number) filter.getValue()));
            }
            else if (filter.getOperator() == Operator.lt
                    && filter.getValue() != null)
            {
                restrictions = criteriaBuilder.and(
                        restrictions,
                        criteriaBuilder.lt(
                                getPath(root, PathType.number,
                                        filter.getProperty()),
                                (Number) filter.getValue()));
            }
            else if (filter.getOperator() == Operator.ge
                    && filter.getValue() != null)
            {
                restrictions = criteriaBuilder.and(
                        restrictions,
                        criteriaBuilder.ge(
                                getPath(root, PathType.number,
                                        filter.getProperty()),
                                (Number) filter.getValue()));
            }
            else if (filter.getOperator() == Operator.le
                    && filter.getValue() != null)
            {
                restrictions = criteriaBuilder.and(
                        restrictions,
                        criteriaBuilder.le(
                                getPath(root, PathType.number,
                                        filter.getProperty()),
                                (Number) filter.getValue()));
            }
            else if (filter.getOperator() == Operator.like
                    && filter.getValue() != null
                    && filter.getValue() instanceof String)
            {
                restrictions = criteriaBuilder.and(
                        restrictions,
                        criteriaBuilder.like(
                                getPath(root, PathType.string,
                                        filter.getProperty()),
                                (String) filter.getValue()));
            }
            else if (filter.getOperator() == Operator.in
                    && filter.getValue() != null)
            {
                restrictions = criteriaBuilder.and(restrictions,
                        getPath(root, PathType.common, filter.getProperty())
                                .in(filter.getValue()));
            }
            else if (filter.getOperator() == Operator.isNull)
            {
                restrictions = criteriaBuilder.and(restrictions,
                        getPath(root, PathType.common, filter.getProperty())
                                .isNull());
            }
            else if (filter.getOperator() == Operator.isNotNull)
            {
                restrictions = criteriaBuilder.and(restrictions,
                        getPath(root, PathType.common, filter.getProperty())
                                .isNotNull());
            }
        }
        criteriaQuery.where(restrictions);
    }

    @SuppressWarnings("unchecked")
    private void addRestrictions(CriteriaQuery<T> criteriaQuery,
            Pageable pageable)
    {
        if (criteriaQuery == null || pageable == null)
        {
            return;
        }
        Root<T> root = getRoot(criteriaQuery);
        if (root == null)
        {
            return;
        }
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        Predicate restrictions = criteriaQuery.getRestriction() != null ? criteriaQuery
                .getRestriction() : criteriaBuilder.conjunction();
        if (StringUtils.isNotEmpty(pageable.getSearchProperty())
                && StringUtils.isNotEmpty(pageable.getSearchValue()))
        {
            restrictions = criteriaBuilder.and(
                    restrictions,
                    criteriaBuilder.like(
                            getPath(root, PathType.string,
                                    pageable.getSearchProperty()), "%"
                                    + pageable.getSearchValue() + "%"));
        }
        if (pageable.getFilters() != null)
        {
            for (Filter filter : pageable.getFilters())
            {
                if (filter == null || StringUtils.isEmpty(filter.getProperty()))
                {
                    continue;
                }
                if (filter.getOperator() == Operator.eq
                        && filter.getValue() != null)
                {
                    if (filter.getIgnoreCase() != null
                            && filter.getIgnoreCase()
                            && filter.getValue() instanceof String)
                    {
                        restrictions = criteriaBuilder.and(restrictions,
                                criteriaBuilder.equal(criteriaBuilder
                                        .lower(getPath(root, PathType.string,
                                                filter.getProperty())),
                                        ((String) filter.getValue())
                                                .toLowerCase()));
                    }
                    else
                    {
                        restrictions = criteriaBuilder.and(
                                restrictions,
                                criteriaBuilder.equal(
                                        getPath(root, PathType.common,
                                                filter.getProperty()),
                                        filter.getValue()));
                    }
                }
                else if (filter.getOperator() == Operator.ne
                        && filter.getValue() != null)
                {
                    if (filter.getIgnoreCase() != null
                            && filter.getIgnoreCase()
                            && filter.getValue() instanceof String)
                    {
                        restrictions = criteriaBuilder.and(restrictions,
                                criteriaBuilder.notEqual(criteriaBuilder
                                        .lower(getPath(root, PathType.string,
                                                filter.getProperty())),
                                        ((String) filter.getValue())
                                                .toLowerCase()));
                    }
                    else
                    {
                        restrictions = criteriaBuilder.and(
                                restrictions,
                                criteriaBuilder.notEqual(
                                        getPath(root, PathType.common,
                                                filter.getProperty()),
                                        filter.getValue()));
                    }
                }
                else if (filter.getOperator() == Operator.gt
                        && filter.getValue() != null)
                {
                    restrictions = criteriaBuilder.and(restrictions,
                            criteriaBuilder.gt(
                                    getPath(root, PathType.number,
                                            filter.getProperty()),
                                    (Number) filter.getValue()));
                }
                else if (filter.getOperator() == Operator.lt
                        && filter.getValue() != null)
                {
                    restrictions = criteriaBuilder.and(restrictions,
                            criteriaBuilder.lt(
                                    getPath(root, PathType.number,
                                            filter.getProperty()),
                                    (Number) filter.getValue()));
                }
                else if (filter.getOperator() == Operator.ge
                        && filter.getValue() != null)
                {
                    restrictions = criteriaBuilder.and(restrictions,
                            criteriaBuilder.ge(
                                    getPath(root, PathType.number,
                                            filter.getProperty()),
                                    (Number) filter.getValue()));
                }
                else if (filter.getOperator() == Operator.le
                        && filter.getValue() != null)
                {
                    restrictions = criteriaBuilder.and(restrictions,
                            criteriaBuilder.le(
                                    getPath(root, PathType.number,
                                            filter.getProperty()),
                                    (Number) filter.getValue()));
                }
                else if (filter.getOperator() == Operator.like
                        && filter.getValue() != null
                        && filter.getValue() instanceof String)
                {
                    restrictions = criteriaBuilder.and(restrictions,
                            criteriaBuilder.like(
                                    getPath(root, PathType.string,
                                            filter.getProperty()),
                                    (String) filter.getValue()));
                }
                else if (filter.getOperator() == Operator.in
                        && filter.getValue() != null)
                {
                    restrictions = criteriaBuilder
                            .and(restrictions,
                                    getPath(root, PathType.common,
                                            filter.getProperty()).in(
                                            filter.getValue()));
                }
                else if (filter.getOperator() == Operator.isNull)
                {
                    restrictions = criteriaBuilder
                            .and(restrictions,
                                    getPath(root, PathType.common,
                                            filter.getProperty()).isNull());
                }
                else if (filter.getOperator() == Operator.isNotNull)
                {
                    restrictions = criteriaBuilder
                            .and(restrictions,
                                    getPath(root, PathType.common,
                                            filter.getProperty()).isNotNull());
                }
            }
        }
        criteriaQuery.where(restrictions);
    }

    private void addOrders(CriteriaQuery<T> criteriaQuery, List<Order> orders)
    {
        if (criteriaQuery == null || orders == null || orders.isEmpty())
        {
            return;
        }
        Root<T> root = getRoot(criteriaQuery);
        if (root == null)
        {
            return;
        }
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        List<javax.persistence.criteria.Order> orderList = new ArrayList<javax.persistence.criteria.Order>();
        if (!criteriaQuery.getOrderList().isEmpty())
        {
            orderList.addAll(criteriaQuery.getOrderList());
        }
        for (Order order : orders)
        {
            if (order.getDirection() == Direction.asc)
            {
                orderList.add(criteriaBuilder.asc(getPath(root,
                        PathType.common, order.getProperty())));
            }
            else if (order.getDirection() == Direction.desc)
            {
                orderList.add(criteriaBuilder.desc(getPath(root,
                        PathType.common, order.getProperty())));
            }
        }
        criteriaQuery.orderBy(orderList);
    }

    private void addOrders(CriteriaQuery<T> criteriaQuery, Pageable pageable)
    {
        if (criteriaQuery == null || pageable == null)
        {
            return;
        }
        Root<T> root = getRoot(criteriaQuery);
        if (root == null)
        {
            return;
        }
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        List<javax.persistence.criteria.Order> orderList = new ArrayList<javax.persistence.criteria.Order>();
        if (!criteriaQuery.getOrderList().isEmpty())
        {
            orderList.addAll(criteriaQuery.getOrderList());
        }
        if (StringUtils.isNotEmpty(pageable.getOrderProperty())
                && pageable.getOrderDirection() != null)
        {
            if (pageable.getOrderDirection() == Direction.asc)
            {
                orderList.add(criteriaBuilder.asc(getPath(root,
                        PathType.common, pageable.getOrderProperty())));
            }
            else if (pageable.getOrderDirection() == Direction.desc)
            {
                orderList.add(criteriaBuilder.desc(getPath(root,
                        PathType.common, pageable.getOrderProperty())));
            }
        }
        if (pageable.getOrders() != null)
        {
            for (Order order : pageable.getOrders())
            {
                if (order.getDirection() == Direction.asc)
                {
                    orderList.add(criteriaBuilder.asc(getPath(root,
                            PathType.common, order.getProperty())));
                }
                else if (order.getDirection() == Direction.desc)
                {
                    orderList.add(criteriaBuilder.desc(getPath(root,
                            PathType.common, order.getProperty())));
                }
            }
        }
        criteriaQuery.orderBy(orderList);
    }

    public void merge(Collection<T> entitys)
    {
        Assert.notNull(entitys);
        for (T t : entitys)
        {
            merge(t);
        }
    }

    public void remove(Collection<T> entitys)
    {
        Assert.notNull(entitys);
        for (T t : entitys)
        {
            remove(t);
        }
    }

    public void persistOrMerge(T entity)
    {
        Assert.notNull(entity);

        if (null != getIdentifier(entity))
        {
            merge(entity);
        }
        else
        {
            persist(entity);
        }
    }

    public void persistOrMerge(Collection<T> entitys)
    {
        Assert.notNull(entitys);

        for (T t : entitys)
        {
            persistOrMerge(t);
        }
    }
}